Skip to content

fix(workspace): restored panes read their persisted ghost log - #1227

Closed
Juliusolsson05 wants to merge 4 commits into
mainfrom
fix/restore-reads-ghost-log
Closed

Juliusolsson05 wants to merge 4 commits into
mainfrom
fix/restore-reads-ghost-log

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Fixes #1225

What was wrong

After a reload, a restored pane never read its ghost log (<userData>/ghost-logs/<id>.ghost.jsonl), so unsent and in-flight prompts disappeared. The only reader was a spawn-time bootstrap. It looked the log up under the new spawn id, which never matches the persisted id. On disk, 1,935 of 1,952 ghost logs were orphans.

Fix

  • initialHistory.ts: readPersistedGhosts(sessionId) runs in parallel with the history read. It merges into loadedGhosts (the in-memory entry wins), then reconcileUpstream runs against the committed turns. Only changed entries are appended back through ghostsToPersist. A read failure degrades to an empty log and logs a warning.
  • session.ts: the dead spawn-time bootstrap (68 lines) is removed.

Evidence

  • Fixture testing/fixtures/ghost-restore/: 7 real unsuperseded ghosts from a live log, plus the 8 committed records of their turns.
  • initialHistory.ghostRestore.renderer.test.tsx:
    • Test 1: the restore supersedes all 7 ghosts.
    • Test 2 (crash case): if a turn's records are dropped, its ghosts stay visible.
  • Mutation check: stubbing the read with an empty Map turns both tests red.
  • sessionRecovery.renderer.test.tsx: the wait on the spawn bootstrap is replaced with not.toHaveBeenCalled.
  • Renderer workspace and session-runtime suites: 1,399 of 1,399 pass. tsc -b is clean.

🤖 Generated with Claude Code

Nothing had read <userData>/ghost-logs/<id>.ghost.jsonl on restore since
0cb71e9: the only reader was a spawn-time bootstrap keyed by the fresh
spawn id, which never matches the persisted log after a reload. Unsent
and in-flight prompts vanished from restored panes, and 1,935 of 1,952
logs on disk were orphans.

The history load now reads the log under the durable session id in
parallel with the transcript, reconciles it against committed turns, and
appends back only entries whose state changed. The dead spawn bootstrap
is removed.

Fixes #1225

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…round-1 fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1227 (blocker): reading the log painted committed turns a
second time at the bottom of restored feeds. Render rule 4 shows an
orphan newer than the committed tail, and a logged ghost's updatedAt is
its orphan time (last delta + 30 s) or, if never orphaned, the restore
itself: both after its own commit. Every Codex text ghost (its response
id never matches the rollout turn id) and Claude tool calls committed
above the loaded window came back.

Restored ghosts are now re-dated to their createdAt and marked orphaned.
A crash-mid-turn ghost began after the last commit and still paints;
any ghost of a turn that committed began before it and is hidden.

Tests use two more real sessions (fixture stale-ghosts-after-restore)
and assert on the merged feed: no stale ghost paints for Claude or
Codex, and the Codex final answer paints when the tail is cut before it.
Dropping the re-date fails all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 2 commits September 25, 2026 00:51
Steering note q6 on #1227: re-dating restored ghosts to createdAt hides
a ghost that began before the last commit and kept streaming after it.
Scanning 7,128 Codex ghosts against their rollouts found 7 such
straddles (the rollout write lags the proxy; one by 34 ms), each of
which a crash would have left as the only copy.

The restore clock is now the ghost's last content update: the newest
updatedAt among its un-orphaned records (Claude logs every update), or
orphanedAt - TTL for a Codex log that holds only the orphan record (an
upper bound, so in doubt it paints). What that bound lets through, a
Codex response that did commit, is removed by identity: the chunk's
token_usage_record names each committed response_id.

GHOST_ORPHAN_TTL_MS moves to ghosts.ts, the one value the sweep and its
inverse share. The recorded straddle joins the fixture; createdAt as the
clock, no identity check, and no re-date each fail a different test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…passed

Review of #1227 (B F1/F2, C1-C5):
- A restored ghost the tail already passed is not merged. Rule 4 hides
  it for good and most can never be superseded (Codex ids, #1231; all
  1,864 OpenCode ghosts on disk), so it was permanent cargo re-merged
  and swept on every restore. Only ghosts newer than the tail can paint.
- Tests now pin: only supersedes are written back (#731 mutation fails),
  the in-memory ghost wins over the log's copy, the crash case drops the
  NEWEST turn and asserts what paints, and the removed deferred spawn
  read is caught after one macrotask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Closing unmerged, by the owner's decision (2026-09-25). The on-disk ghost log is being removed instead of revived.

  • Unused: nothing has read it since 0cb71e9 (2026-07-16), and it had grown to 2.1 GB (1,935 of 1,955 files were orphans).
  • Misleading: the text it would restore after a crash is not in the agent's own resumed conversation.
  • Hard to get right: the review here showed that restoring it correctly needs clock and identity machinery (the q6 straddle, Codex response ids) for a rare case.

In-memory ghosts, the live streaming preview, stay. The removal PR follows. The fixtures and findings on this branch stay on it for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(recovery): restored panes never read their ghost log (crash-resume bootstrap dead since July)

1 participant